<?php

/*
Copyright 2009-2011 Sam Weiss
All Rights Reserved.

This file is part of Spark/Plug.

Spark/Plug is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

if (!defined('spark/plug'))
{
	header('HTTP/1.1 403 Forbidden');
	exit('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don\'t have permission to access the requested resource on this server.</p></body></html>');
}

// -----------------------------------------------------------------------------

class SparkAuthController extends SparkController
{
	private $_authModel;
	private $_bypassMethods;
	private $_authView;
	private $_authViewVars;
	
	// --------------------------------------------------------------------------

	public function __construct($params = NULL)
	{
		parent::__construct();
		
		$this->_authModel = $this->factory->manufacture('SparkAuthModel');
		$this->_bypassMethods = isset($params['auth_bypass']) ? $params['auth_bypass'] : array();
		$this->_bypassMethods['action_login'] = true;
		$this->_authView = isset($params['auth_view']) ? $params['auth_view'] : 'login';
		$this->_authViewVars = isset($params['auth_view_vars']) ? $params['auth_view_vars'] : NULL;
	}

	// --------------------------------------------------------------------------

	public function _before_dispatch($method, $params)
	{
		if (!parent::_before_dispatch($method, $params))
		{
			return false;
		}
		
		// first check list of methods that are allowed to bypass authentication
		
		if (@$this->_bypassMethods[$method] === true)
		{
			return true;
		}
		
		// do we have an authenticated user?
		
		if (!$this->_authModel->authenticate())
		{
			if (!$loginController = $this->config->get('login_controller'))
			{
				$loginController = $this->config->get('default_controller');
			}
			
			$this->session->flashSet('return_url', SparkUtil::self_url());
			$this->redirect("/{$loginController}/login");
		}
		
		return true;
	}

	//----------------------------------------------------------------
	
	public function action_login($params)
	{
		if ($this->_authModel->authenticate())
		{
			$this->redirect('/');
		}

		switch (@$params[0])
		{
			case 'help':
				return $this->_loginHelp($this->dropParam($params));
			case 'change':
				return $this->_loginChange($this->dropParam($params));
			default:
				if (!$params['count'])
				{
					return $this->_login($params);
				}
		}
	
		throw new SparkHTTPException_NotFound(NULL, array('reason'=>"action not found: {$params[0]}"));
	}
	
	//----------------------------------------------------------------
	
	public function action_logout($params)
	{
		if ($this->_authModel->logout($userInfo))
		{
			$this->observer->notify('SparkAuthController:logout', $userInfo);
		}
		$this->redirect('/');
	}
	
	// --------------------------------------------------------------------------

	protected function setAuthView($view, $vars)
	{
		$this->_authView = $view;
		$this->_authViewVars = $vars;
	}
	
	//----------------------------------------------------------------
	
	private function _login($params)
	{
		if (!$returnURL = $this->session->flashKeep('return_url'))
		{
			$returnURL = SparkUtil::self_url();
		}
		
		if (isset($params['pv']['login']))
		{
			if ($this->validateLogin($params['pv'], $this->_authViewVars['errors']))
			{
				$userInfo = array('login'=>$params['pv']['username'], 'password'=>$params['pv']['password']);
				if ($this->_authModel->login($userInfo))
				{
					$this->observer->notify('SparkAuthController:login', $userInfo);
					header('Location: ' . $returnURL);
					exit;
				}
			}
			$this->_authViewVars['username'] = $params['pv']['username'];
			$this->_authViewVars['password'] = $params['pv']['password'];
			$this->_authViewVars['warning'] = 'Could not log in with specified information.';
			if (!empty($this->_authViewVars['errors']))
			{
				$this->_authViewVars['warning'] .= ' (' . implode(', ', $this->_authViewVars['errors']) . ')';
			}
			sleep(5);
		}
		else
		{
			$this->_authViewVars['username'] = '';
			$this->_authViewVars['password'] = '';
		}

		$this->render($this->_authView, $this->_authViewVars);
	}

	//----------------------------------------------------------------
	
	private function _loginHelp($params)
	{
		if (isset($params['pv']['submit']))
		{
			if ($this->validateLoginHelp($params['pv'], $this->_authViewVars['errors']))
			{
				$email = $params['pv']['email'];
				
				// look up account by email address, retrieve account ID and authentication nonce
				
				$model = $this->newModel('user');
				$user = $model->fetchUserByEMail($email);
				
				// if user not found or nonce is empty, we cannot create a confirmation token, so error out
				
				if (!$user || empty($user->nonce))
				{
					$this->_authViewVars['warning'] = 'Sorry, we were unable to create a confirmation request for this account.';
				}
				else
				{
					$confirm = substr(md5($user->nonce), 0, 16).bin2hex($user->id);
					$link = str_replace('/help', '/change/'.$confirm, SparkUtil::self_url());

					if (!$this->sendConfirmationEmail($user, $link))
					{
						$this->_authViewVars['warning'] = 'Sorry, we were unable to send the confirmation email. Please try again later.';
					}
					else
					{
						$this->_authViewVars['success'] = true;
					}
				}
			}
			$this->_authViewVars['email'] = $params['pv']['email'];
			sleep(5);
		}
		else
		{
			$this->_authViewVars['email'] = '';
		}

		$this->_authViewVars['for_help'] = true;			// let view know this is for password help
		$this->render($this->_authView, $this->_authViewVars);
	}
	
	//----------------------------------------------------------------
	
	private function _loginChange($params)
	{
		$confirm = @$params[0];
		
		if (isset($params['pv']['login']))
		{
			if ($this->validateLoginChange($params['pv'], $this->_authViewVars['errors']))
			{
				$id = pack('H*', substr($confirm, 16));
				$md5nonce = substr($confirm, 0, 16);
				
				// look up account by ID address, retrieve login ID and authentication nonce
				
				$model = $this->newModel('user');
				$user = $model->fetchUser($id);
				
				// if user not found or nonce is empty, we cannot create a confirmation token, so error out
				
				if (!$user || empty($user->nonce) || ($params['pv']['username'] !== $user->login) || ($md5nonce !== substr(md5($user->nonce), 0, 16)))
				{
					$this->_authViewVars['warning'] = 'Sorry, we were unable to confirm your request.';
				}
				else
				{
					// update the account password
					
					$user->password = $this->_authModel->encryptPassword($params['pv']['password']);
					$model->updateUser($user);
					
					$userInfo = array('login'=>$params['pv']['username'], 'password'=>$params['pv']['password']);
					if ($this->_authModel->login($userInfo))
					{
						$this->observer->notify('SparkAuthController:login', $userInfo);
						$this->redirect('/');
					}
					$this->_authViewVars['warning'] = 'Sorry, we were unable to log you in.';
				}
			}
			$this->_authViewVars['username'] = $params['pv']['username'];
			$this->_authViewVars['password'] = $params['pv']['password'];
			$this->_authViewVars['repeat_password'] = $params['pv']['repeat_password'];
			sleep(5);
		}
		else
		{
			$this->_authViewVars['username'] = '';
			$this->_authViewVars['password'] = '';
			$this->_authViewVars['repeat_password'] = '';
		}
		
		$this->_authViewVars['for_change'] = true;	// let view know this is for password change
		$this->render($this->_authView, $this->_authViewVars);
	}
	
	// --------------------------------------------------------------------------

	private function validateLogin($params, &$errors)
	{
		if (empty($params['username']))
		{
			$errors['username'] = 'User name is required.';		
		}
		if (empty($params['password']))
		{
			$errors['password'] = 'Password is required.';		
		}

		return empty($errors);
	}
	
	// --------------------------------------------------------------------------

	private function validateLoginHelp($params, &$errors)
	{
		if (empty($params['email']))
		{
			$errors['email'] = 'Email address is required.';		
		}
		elseif (!SparkUtil::valid_email($params['email']))
		{
			$errors['email'] = 'Properly formatted email address is required.';		
		}
		
		return empty($errors);
	}
	
	// --------------------------------------------------------------------------

	private function validateLoginChange($params, &$errors)
	{
		if (empty($params['username']))
		{
			$errors['username'] = 'User name is required.';		
		}
		if (empty($params['password']))
		{
			$errors['password'] = 'New Password is required.';		
		}
		if (empty($params['repeat_password']))
		{
			$errors['repeat_password'] = 'Repeat Password is required.';		
		}
		if (strlen($params['password']) < 8)
		{
			$errors['password'] = 'Password must be at least 8 characters in length.';
		}
		if ($params['password'] !== $params['repeat_password'])
		{
			$errors['repeat_password'] = 'Repeat Password must match Password.';
		}
		
		return empty($errors);
	}
	
	// --------------------------------------------------------------------------

	private function sendConfirmationEmail($user, $link)
	{
		$sender = 'noreply@'.SparkUtil::host();
		$appName = $this->config->get('app_name', 'Spark/Plug');
	
		$body = <<< EOD
Dear {$user->name},

Someone (probably you) has requested a new password for your {$appName} account.

To choose a new password, please follow the confirmation link below.

{$link}

EOD;
	
		$mailer = $this->factory->manufacture('SparkMailer');
		$mailer->isHTML(false)->sender($sender)->from($sender)->fromName($appName)->addAddress($user->email, $user->name);
		$mailer->subject("Password Assistance for your {$appName} Account");
		$mailer->body($body);
		
		try
		{
			$mailer->send();
			return true;
		}
		catch (Exception $e)
		{
		}

		return false;
	}
	
	// --------------------------------------------------------------------------
}
